/*
 * MIT License
 *
 * Copyright (c) 2023 北京凯特伟业科技有限公司
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.je.bpm.spring.boot;

import com.je.bpm.api.runtime.process.event.impl.StartMessageSubscriptionConverter;
import com.je.bpm.api.runtime.process.impl.APIDeploymentConverter;
import com.je.bpm.api.runtime.process.impl.APIProcessDefinitionConverter;
import com.je.bpm.api.runtime.process.impl.ExtensionsVariablesMappingProvider;
import com.je.bpm.api.runtime.process.spring.ProcessVariablesInitiator;
import com.je.bpm.core.process.validation.ProcessValidatorImpl;
import com.je.bpm.core.process.validation.validator.ValidatorSet;
import com.je.bpm.engine.ManagementService;
import com.je.bpm.engine.RepositoryService;
import com.je.bpm.engine.approvalnotice.ActivitiApprovalNotice;
import com.je.bpm.engine.cfg.ProcessEngineConfigurator;
import com.je.bpm.engine.earlyWarning.EarlyWarningPush;
import com.je.bpm.engine.impl.event.EventSubscriptionPayloadMappingProvider;
import com.je.bpm.engine.impl.persistence.StrongUuidGenerator;
import com.je.bpm.engine.upcoming.ActivitiUpcomingRun;
import com.je.bpm.model.process.events.ApplicationDeployedEvent;
import com.je.bpm.model.process.events.ProcessDeployedEvent;
import com.je.bpm.model.process.events.StartMessageDeployedEvent;
import com.je.bpm.runtime.process.event.listener.ProcessRuntimeEventListener;
import com.je.bpm.runtime.shared.RemoteCallServeManager;
import com.je.bpm.runtime.shared.identity.*;
import com.je.bpm.spring.*;
import com.je.bpm.spring.boot.process.validation.AsyncPropertyValidator;
import com.je.bpm.spring.process.ProcessExtensionResourceFinderDescriptor;
import com.je.bpm.spring.project.ApplicationUpgradeContextService;
import com.je.bpm.spring.resources.ResourceFinder;
import com.je.bpm.spring.resources.ResourceFinderDescriptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;

import static java.util.Collections.emptyList;

@Configuration
@AutoConfigureAfter(name = {"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",
        "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration"})
@EnableConfigurationProperties({ActivitiProperties.class, AsyncExecutorProperties.class})
public class ProcessEngineAutoConfiguration extends AbstractProcessEngineAutoConfiguration {

    public static final String BEHAVIOR_FACTORY_MAPPING_CONFIGURER = "behaviorFactoryMappingConfigurer";
    private final UserDepartmentManager userDepartmentManager;
    private final UserRoleManager userRoleManager;
    private final UserOrgManager userOrgManager;
    private final UserPositionManager userPositionManager;
    private final WorkGroupManager workGroupManager;
    private final UserManager userManager;
    private final UserSpecialTreatmentConfigManager userSpecialTreatmentConfigManager;
    protected UserFormFieldsManager userFormFieldsManager;
    protected UserServeCustomizeManager userServeCustomizeManager;
    protected UserSqlManager userSqlManager;
    protected RemoteCallServeManager remoteCallServeManager;
    protected ResultUserParser resultUserParser;
    protected UserNodeValidator userNodeValidator;
    protected ResultPassUserParser resultPassUserParser;
    protected ActivitiUpcomingRun activitiUpcomingRun;
    protected ActivitiApprovalNotice activitiApprovalNotice;
    protected EarlyWarningPush earlyWarningPush;

    public ProcessEngineAutoConfiguration(UserDepartmentManager userDepartmentManager, UserRoleManager userRoleManager,
                                          UserPositionManager userPositionManager, WorkGroupManager workGroupManager, UserOrgManager userOrgManager
            , UserManager userManager, UserSpecialTreatmentConfigManager userSpecialTreatmentConfigManager,
                                          UserFormFieldsManager userFormFieldsManager, UserServeCustomizeManager userServeCustomizeManager,
                                          UserSqlManager userSqlManager, RemoteCallServeManager remoteCallServeManager,
                                          ResultUserParser resultUserParser, ResultPassUserParser resultPassUserParser
            , ActivitiUpcomingRun activitiUpcomingRun, ActivitiApprovalNotice activitiApprovalNotice, UserNodeValidator userNodeValidator
            , EarlyWarningPush earlyWarningPush
    ) {
        this.userDepartmentManager = userDepartmentManager;
        this.userRoleManager = userRoleManager;
        this.userPositionManager = userPositionManager;
        this.workGroupManager = workGroupManager;
        this.userOrgManager = userOrgManager;
        this.userManager = userManager;
        this.userSpecialTreatmentConfigManager = userSpecialTreatmentConfigManager;
        this.userServeCustomizeManager = userServeCustomizeManager;
        this.userFormFieldsManager = userFormFieldsManager;
        this.userSqlManager = userSqlManager;
        this.remoteCallServeManager = remoteCallServeManager;
        this.resultUserParser = resultUserParser;
        this.activitiUpcomingRun = activitiUpcomingRun;
        this.resultPassUserParser = resultPassUserParser;
        this.activitiApprovalNotice = activitiApprovalNotice;
        this.userNodeValidator = userNodeValidator;
        this.earlyWarningPush = earlyWarningPush;
    }

    @Bean
    @ConditionalOnMissingBean
    public SpringProcessEngineConfiguration springProcessEngineConfiguration(DataSource dataSource,
                                                                             PlatformTransactionManager transactionManager,
                                                                             SpringAsyncExecutor springAsyncExecutor,
                                                                             ActivitiProperties activitiProperties,
                                                                             ResourceFinder resourceFinder,
                                                                             List<ResourceFinderDescriptor> resourceFinderDescriptors,
                                                                             ApplicationUpgradeContextService applicationUpgradeContextService,
                                                                             @Autowired(required = false) List<ProcessEngineConfigurationConfigurer> processEngineConfigurationConfigurers,
                                                                             @Autowired(required = false) List<ProcessEngineConfigurator> processEngineConfigurators) throws IOException {

        SpringProcessEngineConfiguration conf = new SpringProcessEngineConfiguration(applicationUpgradeContextService);
        conf.setConfigurators(processEngineConfigurators);
        configureResources(resourceFinder, resourceFinderDescriptors, conf);
        conf.setDataSource(dataSource);
        conf.setTransactionManager(transactionManager);
        conf.setAsyncExecutor(springAsyncExecutor);
        conf.setDeploymentName(activitiProperties.getDeploymentName());
        conf.setDatabaseSchema(activitiProperties.getDatabaseSchema());
        conf.setDatabaseSchemaUpdate(activitiProperties.getDatabaseSchemaUpdate());
        conf.setDbHistoryUsed(activitiProperties.isDbHistoryUsed());
        conf.setAsyncExecutorActivate(activitiProperties.isAsyncExecutorActivate());
        addAsyncPropertyValidator(activitiProperties, conf);

        conf.setResultUserParser(resultUserParser);
        conf.setUserNodeValidator(userNodeValidator);
        conf.setUserDepartmentManager(userDepartmentManager);
        conf.setUserRoleManager(userRoleManager);
        conf.setUserOrgManager(userOrgManager);
        conf.setUserPositionManager(userPositionManager);
        conf.setWorkGroupManager(workGroupManager);
        conf.setUserManager(userManager);
        conf.setEarlyWarningPush(earlyWarningPush);
        conf.setUserSpecialTreatmentConfigManager(userSpecialTreatmentConfigManager);
        conf.setUserFormFieldsManager(userFormFieldsManager);
        conf.setUserServeCustomizeManager(userServeCustomizeManager);
        conf.setUserSqlManager(userSqlManager);
        conf.setRemoteCallServeManager(remoteCallServeManager);
        conf.setResultPassUserParser(resultPassUserParser);
        conf.setActivitiUpcomingRun(activitiUpcomingRun);
        conf.setActivitiApprovalNotice(activitiApprovalNotice);
        conf.setHistoryLevel(activitiProperties.getHistoryLevel());
        conf.setCopyVariablesToLocalForTasks(activitiProperties.isCopyVariablesToLocalForTasks());
        conf.setSerializePOJOsInVariablesToJson(activitiProperties.isSerializePOJOsInVariablesToJson());
        conf.setJavaClassFieldForJackson(activitiProperties.getJavaClassFieldForJackson());

        if (activitiProperties.getCustomMybatisMappers() != null) {
            conf.setCustomMybatisMappers(getCustomMybatisMapperClasses(activitiProperties.getCustomMybatisMappers()));
        }

        if (activitiProperties.getCustomMybatisXMLMappers() != null) {
            conf.setCustomMybatisXMLMappers(new HashSet<>(activitiProperties.getCustomMybatisXMLMappers()));
        }

        if (activitiProperties.getCustomMybatisXMLMappers() != null) {
            conf.setCustomMybatisXMLMappers(new HashSet<>(activitiProperties.getCustomMybatisXMLMappers()));
        }

        if (activitiProperties.isUseStrongUuids()) {
            conf.setIdGenerator(new StrongUuidGenerator());
        }

        if (activitiProperties.getDeploymentMode() != null) {
            conf.setDeploymentMode(activitiProperties.getDeploymentMode());
        }

        if (processEngineConfigurationConfigurers != null) {
            for (ProcessEngineConfigurationConfigurer processEngineConfigurationConfigurer : processEngineConfigurationConfigurers) {
                processEngineConfigurationConfigurer.configure(conf);
            }
        }
        springAsyncExecutor.applyConfig(conf);
        return conf;
    }

    private void configureResources(ResourceFinder resourceFinder, List<ResourceFinderDescriptor> resourceFinderDescriptors, SpringProcessEngineConfiguration conf) throws IOException {
        List<Resource> resources = new ArrayList<>();
        for (ResourceFinderDescriptor resourceFinderDescriptor : resourceFinderDescriptors) {
            resources.addAll(resourceFinder.discoverResources(resourceFinderDescriptor));
        }
        conf.setDeploymentResources(resources.toArray(new Resource[0]));
    }

    protected void addAsyncPropertyValidator(ActivitiProperties activitiProperties, SpringProcessEngineConfiguration conf) {
        if (!activitiProperties.isAsyncExecutorActivate()) {
            ValidatorSet springBootStarterValidatorSet = new ValidatorSet("activiti-spring-boot-starter");
            springBootStarterValidatorSet.addValidator(new AsyncPropertyValidator());
            if (conf.getProcessValidator() == null) {
                ProcessValidatorImpl processValidator = new ProcessValidatorImpl();
                processValidator.addValidatorSet(springBootStarterValidatorSet);
                conf.setProcessValidator(processValidator);
            } else {
                conf.getProcessValidator().getValidatorSets().add(springBootStarterValidatorSet);
            }
        }
    }

    @Bean
    @ConditionalOnMissingBean
    public ProcessDefinitionResourceFinderDescriptor processDefinitionResourceFinderDescriptor(ActivitiProperties activitiProperties) {
        return new ProcessDefinitionResourceFinderDescriptor(activitiProperties);
    }

    @Bean
    @ConditionalOnMissingBean
    public ProcessExtensionResourceFinderDescriptor processExtensionResourceFinderDescriptor(ActivitiProperties activitiProperties,
                                                                                             @Value("${spring.activiti.process.extensions.dir:NOT_DEFINED}") String locationPrefix,
                                                                                             @Value("${spring.activiti.process.extensions.suffix:**-extensions.json}") String locationSuffix) {
        if (locationPrefix.equalsIgnoreCase("NOT_DEFINED")) {
            locationPrefix = activitiProperties.getProcessDefinitionLocationPrefix();
        }
        return new ProcessExtensionResourceFinderDescriptor(activitiProperties.isCheckProcessDefinitions(), locationPrefix, locationSuffix);
    }

    @Bean
    @ConditionalOnMissingBean
    public ProcessDeployedEventProducer processDeployedEventProducer(RepositoryService repositoryService,
                                                                     APIProcessDefinitionConverter converter,
                                                                     @Autowired(required = false) List<ProcessRuntimeEventListener<ProcessDeployedEvent>> listeners,
                                                                     ApplicationEventPublisher eventPublisher) {
        return new ProcessDeployedEventProducer(repositoryService, converter, Optional.ofNullable(listeners).orElse(emptyList()), eventPublisher);
    }

    @Bean
    @ConditionalOnMissingBean
    public StartMessageDeployedEventProducer startMessageDeployedEventProducer(RepositoryService repositoryService,
                                                                               ManagementService managementService,
                                                                               StartMessageSubscriptionConverter subscriptionConverter,
                                                                               APIProcessDefinitionConverter converter,
                                                                               List<ProcessRuntimeEventListener<StartMessageDeployedEvent>> listeners,
                                                                               ApplicationEventPublisher eventPublisher) {
        return new StartMessageDeployedEventProducer(repositoryService, managementService, subscriptionConverter, converter, listeners, eventPublisher);
    }

    @Bean(name = BEHAVIOR_FACTORY_MAPPING_CONFIGURER)
    @ConditionalOnMissingBean(name = BEHAVIOR_FACTORY_MAPPING_CONFIGURER)
    public DefaultActivityBehaviorFactoryMappingConfigurer defaultActivityBehaviorFactoryMappingConfigurer(ExtensionsVariablesMappingProvider variablesMappingProvider,
                                                                                                           ProcessVariablesInitiator processVariablesInitiator,
                                                                                                           EventSubscriptionPayloadMappingProvider eventSubscriptionPayloadMappingProvider) {
        return new DefaultActivityBehaviorFactoryMappingConfigurer(variablesMappingProvider, processVariablesInitiator, eventSubscriptionPayloadMappingProvider);
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ProcessEngineConfigurationConfigurer asyncExecutorPropertiesConfigurer(AsyncExecutorProperties properties) {
        return (configuration) -> {
            configuration.setAsyncExecutorMessageQueueMode(properties.isMessageQueueMode());
            configuration.setAsyncExecutorCorePoolSize(properties.getCorePoolSize());
            configuration.setAsyncExecutorAsyncJobLockTimeInMillis(properties.getAsyncJobLockTimeInMillis());
            configuration.setAsyncExecutorNumberOfRetries(properties.getNumberOfRetries());

            configuration.setAsyncExecutorDefaultAsyncJobAcquireWaitTime(properties.getDefaultAsyncJobAcquireWaitTimeInMillis());
            configuration.setAsyncExecutorDefaultTimerJobAcquireWaitTime(properties.getDefaultTimerJobAcquireWaitTimeInMillis());
            configuration.setAsyncExecutorDefaultQueueSizeFullWaitTime(properties.getDefaultQueueSizeFullWaitTime());

            configuration.setAsyncExecutorMaxAsyncJobsDuePerAcquisition(properties.getMaxAsyncJobsDuePerAcquisition());
            configuration.setAsyncExecutorMaxTimerJobsPerAcquisition(properties.getMaxTimerJobsPerAcquisition());
            configuration.setAsyncExecutorMaxPoolSize(properties.getMaxPoolSize());

            configuration.setAsyncExecutorResetExpiredJobsInterval(properties.getResetExpiredJobsInterval());
            configuration.setAsyncExecutorResetExpiredJobsPageSize(properties.getResetExpiredJobsPageSize());

            configuration.setAsyncExecutorSecondsToWaitOnShutdown(properties.getSecondsToWaitOnShutdown());
            configuration.setAsyncExecutorThreadKeepAliveTime(properties.getKeepAliveTime());
            configuration.setAsyncExecutorTimerLockTimeInMillis(properties.getTimerLockTimeInMillis());
            configuration.setAsyncExecutorThreadPoolQueueSize(properties.getQueueSize());

            configuration.setAsyncFailedJobWaitTime(properties.getRetryWaitTimeInMillis());
        };
    }

    @Bean
    @ConditionalOnMissingBean
    public ApplicationDeployedEventProducer applicationDeployedEventProducer(RepositoryService repositoryService,
                                                                             APIDeploymentConverter converter,
                                                                             @Autowired(required = false) List<ProcessRuntimeEventListener<ApplicationDeployedEvent>> listeners,
                                                                             ApplicationEventPublisher eventPublisher) {
        return new ApplicationDeployedEventProducer(repositoryService,
                converter,
                Optional.ofNullable(listeners)
                        .orElse(emptyList()),
                eventPublisher);
    }

}
